/*
 *  Copyright 2009 Jamie Gennis <jgennis@gmail.com>.
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *       http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *  under the License.
 */

package com.google.code.gwt.remoteaction.merge.client;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.management.RuntimeErrorException;

import com.google.code.gwt.remoteaction.client.AbstractActionProxy;
import com.google.code.gwt.remoteaction.client.Action;
import com.google.code.gwt.remoteaction.client.ActionFilter;
import com.google.code.gwt.remoteaction.client.ActionResult;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.rpc.AsyncCallback;

/**
 * An action filter that merges multiple equivelent in-flight action invocations
 * into a single invocation.
 * 
 * <p>
 * Two in-flight action invocations will be merged if they:
 * <ol>
 * <li>implement the {@link MergeableAction} marker interface</li>
 * <li>are equivelent according to {@link Object#equals(java.lang.Object)}</li>
 * </ol>
 * </p>
 * 
 * <p>
 * The callback for each of the action invocations is called when the single
 * merged action invocation has completed. The one exception to this is if one
 * of the callbacks throws an unchecked exception. In that case some of the
 * callbacks may not be called. The order in which the callbacks are called is
 * not defined.
 * </p>
 * 
 * @author Jamie Gennis <jgennis@gmail.com>
 */
public class MergingActionFilter extends AbstractActionProxy implements ActionFilter {

  class MergedCallback implements AsyncCallback<ActionResult> {

    final MergeableAction action;

    final List<AsyncCallback<ActionResult>> callbacks =
        new ArrayList<AsyncCallback<ActionResult>>();

    public MergedCallback(MergeableAction action, AsyncCallback<ActionResult> callback) {
      this.action = action;
      this.callbacks.add(callback);
    }

    public void onFailure(final Throwable caught) {
      removeMergedCallback(action);
      for (final AsyncCallback<ActionResult> callback : callbacks) {
        /*
         * Use a deferred command so that any exceptions thrown by the callbacks
         * do not interfere with the execution of the other callbacks.
         */
        addDeferredCommand(new Command() {

          @Override
          public void execute() {
            callback.onFailure(caught);
          }
        });
      }
    }

    public void onSuccess(final ActionResult result) {
      removeMergedCallback(action);
      for (final AsyncCallback<ActionResult> callback : callbacks) {
        /*
         * Use a deferred command so that any exceptions thrown by the callbacks
         * do not interfere with the execution of the other callbacks.
         */
        addDeferredCommand(new Command() {

          @Override
          public void execute() {
            callback.onSuccess(result);
          }
        });
      }
    }

    public boolean addCallback(AsyncCallback<ActionResult> e) {
      return callbacks.add(e);
    }
  }

  final Map<MergeableAction, MergedCallback> mergedCallbacks =
      new HashMap<MergeableAction, MergedCallback>();

  /**
   * Execute an action, merging it with other equivelent in-flight action
   * invocations when possible.
   * 
   * If {@code action} can not be merged with any other in-flight action
   * invocations then it is passed on to the next action executor in the chain
   * unchanged. If {@code action} can be merged with a previous action
   * invocation whose callback has not yet been called, then {@code action} is
   * not passed to the next action executor, and {@code callback} will be called
   * once the previous action invocation has completed.
   * 
   * @param action
   * @param callback
   */
  public void execute(Action action, AsyncCallback<ActionResult> callback) {
    if (action instanceof MergeableAction) {
      MergeableAction mergeableAction = (MergeableAction) action;
      MergedCallback mergedCallback = mergedCallbacks.get(mergeableAction);
      if (mergedCallback == null) {
        mergedCallback = new MergedCallback(mergeableAction, callback);
        mergedCallbacks.put(mergeableAction, mergedCallback);
        try {
          proxyExecution(action, mergedCallback);
        } catch (RuntimeException ex) {
          // Don't merge future calls with this one, as it will never complete.
          removeMergedCallback(mergeableAction);
          throw ex;
        } catch (Error err) {
          // Don't merge future calls with this one, as it will never complete.
          removeMergedCallback(mergeableAction);
          throw err;
        }
      } else {
        mergedCallback.addCallback(callback);
      }
    } else {
      proxyExecution(action, callback);
    }
  }

  /**
   * Remove a merged callback from the map containing all in-flight callbacks.
   * 
   * @param action the action instance with which the merged callback is
   *          associated
   */
  void removeMergedCallback(MergeableAction action) {
    mergedCallbacks.remove(action);
  }

  /**
   * Enqueue a deferred command for execution.
   * 
   * <p>
   * This method exists so that it can be mocked for unit tests.
   * </p>
   */
  void addDeferredCommand(Command command) {
    DeferredCommand.addCommand(command);
  }
}
